package com.google.bitcoin.core;

import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.core.Transaction;
import com.google.bitcoin.core.TransactionOutput;
import com.google.bitcoin.core.ScriptException;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.NetworkParameters;

import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.bitcoin.store.WalletPonySerializer;

import java.io.InputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.io.IOException;

import java.util.ArrayList;
import java.util.Random;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
import java.util.LinkedList;
import java.util.TreeSet;
import java.util.Collection;
import java.math.BigInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;



import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString;

public class WalletPony extends Wallet
{
    public static final long serialVersionUID = 6L;
    private static final Logger log = LoggerFactory.getLogger(WalletPony.class);

    private transient Random rnd=new Random();
    private transient ArrayList<Address> change_addresses_list;
    private transient Set<Address> change_addresses_set;
    private transient Set<Address> proscribed_from_addresses;

    private BigInteger fee=BigInteger.valueOf(50000L);

    public WalletPony(NetworkParameters params)
    {
        super(params);
    }
    public void setTxFee(BigInteger f)
    {
        this.fee = f;
    }
    

    public void setChangeAddresses(Collection<Address> new_list)
    {
        ArrayList<Address> lst = new ArrayList<Address>();
        lst.addAll(new_list);

        HashSet<Address> set = new HashSet<Address>();
        set.addAll(new_list);

        change_addresses_list = lst;
        change_addresses_set = set;

    }
    public void setChangeAddress(Address change_address)
    {
        ArrayList<Address> lst = new ArrayList<Address>();
        lst.add(change_address);
        setChangeAddresses(lst);

    }

    public ArrayList<Address> getChangeAddresses()
    {
        return change_addresses_list;
    }
    public Set<Address> getChangeAddressSet()
    {
        return change_addresses_set;

    }

    public synchronized Set<Address> getProscribedFromAddresses()
    {
        return proscribed_from_addresses;

    }
    public void setProscribedFromAddresses(Collection<Address> proscribed)
    {
        HashSet<Address> set = new HashSet<Address>();
        set.addAll(proscribed);

        proscribed_from_addresses = set;
        
    }

    public String toString()
    {
        StringBuilder sb=new StringBuilder();
        sb.append(super.toString());
        sb.append("ChangeAddresses: " + change_addresses_list);
        sb.append('\n');
        sb.append("ProscribedFromAddresses: " + proscribed_from_addresses);
        sb.append('\n');
        sb.append("TXFee: " + fee);
        sb.append('\n');

        return sb.toString();
    }

    public synchronized Address getChangeAddress() {
        ArrayList<Address> lst = null;
        
        if (change_addresses_list != null)
        {
            lst = change_addresses_list;

        }
        else
        {
            lst = new ArrayList<Address>();

            for(ECKey key : keychain)
            {
                lst.add(key.toAddress(getParams()));
            }
        }
        assert lst.size() > 0 : "Can't send value without an address to use for receiving change";
        if (rnd == null) rnd = new Random();
        return lst.get(rnd.nextInt(lst.size()));
    }


    public synchronized void saveToFile(File f) throws IOException 
    {
        File dir = f.getParentFile();

        if (dir==null) dir=new File(".");


        File tmp = File.createTempFile("bitcoin-",".wallet", dir);

        super.saveToFile(tmp);

        /*ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(tmp));
        out.writeObject(this);
        out.flush();
        out.close();*/
        tmp.renameTo(f);
    }

    @Override
    public synchronized void saveToFileStream(OutputStream f) throws IOException 
    {
        WalletProtobufSerializer serial = new WalletProtobufSerializer();
        serial.setWalletExtensionSerializer(new WalletPonySerializer());
        serial.writeWallet(this, f);
    }

    public static WalletPony loadFromFileStream(InputStream stream) throws IOException
    {
        WalletProtobufSerializer serial = new WalletProtobufSerializer();
        serial.setWalletExtensionSerializer(new WalletPonySerializer());
        WalletPony wallet = (WalletPony)serial.readWallet(stream);
        return wallet;

    }
    public static WalletPony loadFromFile(File f) throws IOException {
        return loadFromFileStream(new FileInputStream(f));
    }


    public BigInteger getFee()
    {
        return fee;
    }

    public boolean completeTx(Transaction sendTx, Address changeAddress) {
        return completeTx(sendTx, changeAddress, null, null, null);
    }
    /**
     * Takes a transaction with arbitrary outputs, gathers the necessary inputs for spending, and signs it
     * @param sendTx           The transaction to complete
     * @param changeAddress    Which address to send the change to, in case we can't make exactly the right value from
     *                         our coins. This should be an address we own (is in the keychain).
     * @param outs_to_use      A list of transaction outputs to use in this transaction.
     * @return False if we cannot afford this send, true otherwise
     */
    public synchronized boolean completeTx(Transaction sendTx, Address changeAddress, Address altChangeAddress, BigInteger altChangeLimit, List<TransactionOutput> outs_to_use) {
        // Calculate the transaction total
        BigInteger fee = getFee();
        BigInteger output_amt = BigInteger.ZERO;

        for(TransactionOutput output : sendTx.getOutputs()) {
            output_amt = output_amt.add(output.getValue());
        }
        //Make sure we gather enough for fees
        BigInteger nanocoins = fee.multiply(BigInteger.valueOf(100)).add(output_amt);
        Set<Address> proscribed_out_addresses = getProscribedFromAddresses();

        log.info("Completing send tx with {} outputs totalling {}", sendTx.getOutputs().size(), bitcoinValueToFriendlyString(nanocoins));

        // To send money to somebody else, we need to do gather up transactions with unspent outputs until we have
        // sufficient value. Many coin selection algorithms are possible, we use a simple but suboptimal one.
        // TODO: Sort coins so we use the smallest first, to combat wallet fragmentation and reduce fees.
        BigInteger valueGathered = BigInteger.ZERO;
        List<TransactionOutput> gathered = new LinkedList<TransactionOutput>();
        Set<String> used_outputs=new TreeSet<String>();

        if (outs_to_use != null)
        {
            for(TransactionOutput output : outs_to_use)
            {
                gathered.add(output);
                valueGathered = valueGathered.add(output.getValue());
                used_outputs.add(output.toString());
            }
        }
        if (proscribed_out_addresses==null) proscribed_out_addresses=new HashSet<Address>();

        LinkedList<Transaction> tx_lst=new LinkedList<Transaction>();
        //We will go through the unspent first
        tx_lst.addAll(unspent.values());

        //And then try to fill it from pending
        tx_lst.addAll(pending.values());

        for (Transaction tx : tx_lst) {
            for (TransactionOutput output : tx.getOutputs()) {
                if (valueGathered.compareTo(nanocoins) >= 0) break;
                //spend pending transactions, we don't give a shit
                //if (!output.isAvailableForSpending()) continue;
                if (output.getSpentBy()!=null) continue;
                if (!output.isMine(this)) continue;
                if (used_outputs.contains(output.toString())) continue;
                try
                {
                    if (proscribed_out_addresses.contains(output.getScriptPubKey().getToAddress())) continue;
                }
                catch(Exception e)
                {
                    e.printStackTrace();
                    return false;
                }

                gathered.add(output);
                valueGathered = valueGathered.add(output.getValue());
            }
            if (valueGathered.compareTo(nanocoins) >= 0) break;
        }
        // Can we afford this?
        if (valueGathered.compareTo(nanocoins) < 0) {
            log.info("Insufficient value in wallet for send, missing " +
                    bitcoinValueToFriendlyString(nanocoins.subtract(valueGathered)));
            // TODO: Should throw an exception here.
            return false;
        }
        assert gathered.size() > 0;
        sendTx.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN);
        int thing_count = gathered.size() + 2;
        int fee_multiplier = Math.max(1, thing_count/5);
        System.out.println("Things: " + thing_count + " Fee multiplier: " + fee_multiplier);

        nanocoins = output_amt.add(fee.multiply(BigInteger.valueOf(fee_multiplier)));

        BigInteger change = valueGathered.subtract(nanocoins);
        if (change.compareTo(BigInteger.ZERO) > 0) {
            // The value of the inputs is greater than what we want to send. Just like in real life then,
            // we need to take back some coins ... this is called "change". Add another output that sends the change
            // back to us.
            log.info("  with " + bitcoinValueToFriendlyString(change) + " coins change");
            if ((altChangeAddress!=null) && (!changeAddress.equals(altChangeAddress)) && (change.compareTo(altChangeLimit) > 0))
            {
                BigInteger half = change.divide(BigInteger.valueOf(2));
                sendTx.addOutput(new TransactionOutput(getParams(), sendTx, half, altChangeAddress));
                change = change.subtract(half);
                sendTx.addOutput(new TransactionOutput(getParams(), sendTx, change, changeAddress));
            }
            else
            {
                sendTx.addOutput(new TransactionOutput(getParams(), sendTx, change, changeAddress));
            }
        }
        for (TransactionOutput output : gathered) {
            sendTx.addInput(output);
        }

        // Now sign the inputs, thus proving that we are entitled to redeem the connected outputs.
        try {
            sendTx.signInputs(Transaction.SigHash.ALL, this);
        } catch (ScriptException e) {
            // If this happens it means an output script in a wallet tx could not be understood. That should never
            // happen, if it does it means the wallet has got into an inconsistent state.
            throw new RuntimeException(e);
        }
        log.info("  completed {}", sendTx.getHashAsString());
        return true;
    }

    public synchronized boolean isTransactionRelevant(Transaction tx,
                                                      boolean includeDoubleSpending) throws ScriptException {
        boolean res = super.isTransactionRelevant(tx, includeDoubleSpending);

        if (!res)
        {
            for(TransactionInput ti : tx.getInputs())
            {
                Address from = ti.getFromAddress();
                if (isPubKeyHashMine( from.getHash160() )) res=true;
            }
        }
        return res;
    }

    /**
     * Find unspent transaction outputs on the given address
     */
    public synchronized List<TransactionOutput> getUnspentByAddress(Address addr)
    {
        List<TransactionOutput> lst = new LinkedList<TransactionOutput>();
        for(Transaction tx : unspent.values())
        {
            for(TransactionOutput tx_out : tx.getOutputs())
            {
                try
                {
                    if (tx_out.getSpentBy()==null)
                    if (tx_out.getScriptPubKey().getToAddress().equals(addr))
                    {
                        lst.add(tx_out);
                    }
                }
                catch(com.google.bitcoin.core.ScriptException e)
                {}
            }
        }
        return lst;
    }




}
